package dapp

import (
	"strconv"

	"github.com/spf13/cobra"
	"hundsun.com/hsl/hschain/cmd/cli/common"
	"hundsun.com/hsl/hschain/common/util"
	protosledger "hundsun.com/hsl/hschain/protos/ledger"
	protosrpc "hundsun.com/hsl/hschain/protos/rpc"
	service "hundsun.com/hsl/hschain/rpc/impl"
)

// BlockCmd block command
func BlockCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "block",
		Short: "Block operation",
		Args:  cobra.MinimumNArgs(1),
	}

	cmd.AddCommand(
		GetBlockHeightCmd(),
		GetBlockExecHeightCmd(),
		GetBlockByNumberCmd(),
		GetExecutedBlockByNumberCmd(),
		GetExecutedBlocksCmd(),
		GetIsSynced(),
		GetLatestExecutedBlock(),
		GetBlockExecutedResults(),
	)

	return cmd
}

//StartEndCmd common start end cmd
func StartEndCmd(cmd *cobra.Command) {
	cmd.Flags().StringP("start", "s", "", "start block number")
	cmd.Flags().StringP("end", "e", "", "end block number")
	cmd.MarkFlagRequired("start")
	cmd.MarkFlagRequired("end")
}

//GetStart get start parameter
func GetStart(cmd *cobra.Command) string {
	start, _ := cmd.Flags().GetString("start")
	return start
}

//GetEnd get end parameter
func GetEnd(cmd *cobra.Command) string {
	end, _ := cmd.Flags().GetString("end")
	return end
}

//ExcludeCmd if exclude transactions in the result
func ExcludeCmd(cmd *cobra.Command) {
	cmd.Flags().BoolP("excludeTx", "x", false, "exclude transactions in the result")
}

//GetExclude get excludeTx parameter
func GetExclude(cmd *cobra.Command) bool {
	exclude, _ := cmd.Flags().GetBool("excludeTx")
	return exclude
}

//NumberCmd add blockNumber parameter
func NumberCmd(cmd *cobra.Command) {
	cmd.Flags().StringP("number", "n", "", "block number")
	cmd.MarkFlagRequired("number")
}

//GetNumber get blockNumber parameter
func GetNumber(cmd *cobra.Command) string {
	number, _ := cmd.Flags().GetString("number")
	return number
}

//GetExecutedBlocksCmd get latest executed block
func GetExecutedBlocksCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "getExecBlks",
		Short: "Get executed blocks",
		Run:   getExecutedBlocks,
	}
	StartEndCmd(cmd)
	ExcludeCmd(cmd)
	return cmd
}

//GetBlockExecutedResults get latest executed block
func GetBlockExecutedResults() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "getBlkExecRes",
		Short: "Get block executed result",
		Run:   getBlockExecutedResults,
	}
	StartEndCmd(cmd)
	return cmd
}

//GetLatestExecutedBlock get latest executed block
func GetLatestExecutedBlock() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "getLatestExecBlk",
		Short: "Get latest executed block",
		Run:   getLatestExecutedBlock,
	}
	ExcludeCmd(cmd)
	return cmd
}

// GetBlockByNumberCmd  get block by number
func GetBlockByNumberCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "getBlk",
		Short: "Get block by height",
		Run:   getBlockByNumber,
	}

	NumberCmd(cmd)
	ExcludeCmd(cmd)
	return cmd
}

//GetBlockHeightCmd get height
func GetBlockHeightCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "height",
		Short: "Get current block height",
		Run:   getBlockHeight,
	}
	return cmd
}

//GetBlockExecHeightCmd get exec height
func GetBlockExecHeightCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "execHeight",
		Short: "Get current block exec height",
		Run:   getBlockExecHeight,
	}
	return cmd
}

//GetExecutedBlockByNumberCmd get executed block by number
func GetExecutedBlockByNumberCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "getExecBlk",
		Short: "Get executed block by height",
		Run:   getExecutedBlockByNumber,
	}
	NumberCmd(cmd)
	ExcludeCmd(cmd)
	return cmd
}

//GetIsSynced chain is synced
func GetIsSynced() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "isSynced",
		Short: "Check node is synced",
		Run:   isSynced,
	}
	return cmd
}

func getBlockHeight(cmd *cobra.Command, args []string) {
	rpcLaddr, _ := cmd.Flags().GetString(common.FlagRPCLaddr)
	common.RPCCall(rpcLaddr, service.GetBlockNumber, nil, nil)
}

func getBlockExecHeight(cmd *cobra.Command, args []string) {
	rpcLaddr, _ := cmd.Flags().GetString(common.FlagRPCLaddr)
	common.RPCCall(rpcLaddr, service.GetBlockExecNumber, nil, nil)
}

func getBlockExecutedResults(cmd *cobra.Command, args []string) {
	start := GetStart(cmd)
	end := GetEnd(cmd)
	param := &protosrpc.BlockNumberRangeParam{
		StartBlockNumber: start,
		EndBlockNumber:   end,
	}

	rpcLaddr, _ := cmd.Flags().GetString(common.FlagRPCLaddr)
	common.RPCCall(rpcLaddr, service.GetHistoryBlockExeResult, param,
		func(res interface{}) {
			obj := res.(service.JSONRPCResp)
			var arr []*common.BlockExecResult
			val, _ := util.Bytes2Struct(&protosrpc.BlockExecResults{}, obj.Payload)
			for _, item := range val.(*protosrpc.BlockExecResults).Results {
				hashOpBlkExecResult(item)
				arr = append(arr, common.FromPbBlockExecResult(item))
			}
			common.PrintJSON(arr)
		})
}

func getLatestExecutedBlock(cmd *cobra.Command, args []string) {
	exclude := GetExclude(cmd)
	rpcLaddr, _ := cmd.Flags().GetString(common.FlagRPCLaddr)
	common.RPCCall(rpcLaddr, service.GetLatestExecutedBlock, nil,
		func(res interface{}) {
			obj := res.(service.JSONRPCResp)
			val, _ := util.Bytes2Struct(&protosledger.ExecutedBlock{}, obj.Payload)
			hashOpExecBlk(val.(*protosledger.ExecutedBlock), exclude)
			common.PrintJSON(common.FromPbExecutedBlock(val.(*protosledger.ExecutedBlock)))
		})
}

func getBlockByNumber(cmd *cobra.Command, args []string) {
	number := GetNumber(cmd)
	exclude := GetExclude(cmd)
	param := &protosrpc.BlockNumberParam{
		BlockNumber: number,
	}

	rpcLaddr, _ := cmd.Flags().GetString(common.FlagRPCLaddr)
	common.RPCCall(rpcLaddr, service.GetBlockByNumber, param,
		func(res interface{}) {
			obj := res.(service.JSONRPCResp)
			val, _ := util.Bytes2Struct(&protosledger.Block{}, obj.Payload)
			hashOpBlk(val.(*protosledger.Block), exclude)
			common.PrintJSON(common.FromPbBlock(val.(*protosledger.Block)))
		})
}

func getExecutedBlocks(cmd *cobra.Command, args []string) {
	start := GetStart(cmd)
	end := GetEnd(cmd)
	exclude := GetExclude(cmd)
	param := &protosrpc.BlockNumberRangeParam{
		StartBlockNumber: start,
		EndBlockNumber:   end,
	}

	rpcLaddr, _ := cmd.Flags().GetString(common.FlagRPCLaddr)
	common.RPCCall(rpcLaddr, service.GetExecutedBlocks, param,
		func(res interface{}) {
			obj := res.(service.JSONRPCResp)
			val, _ := util.Bytes2Struct(&protosrpc.ExecutedBlocks{}, obj.Payload)
			var arr []*common.ExecutedBlock
			for _, item := range val.(*protosrpc.ExecutedBlocks).Results {
				hashOpExecBlk(item, exclude)
				arr = append(arr, common.FromPbExecutedBlock(item))
			}
			common.PrintJSON(arr)
		})
}

func getExecutedBlockByNumber(cmd *cobra.Command, args []string) {
	number := GetNumber(cmd)
	exclude := GetExclude(cmd)
	param := &protosrpc.BlockNumberParam{
		BlockNumber: number,
	}

	rpcLaddr, _ := cmd.Flags().GetString(common.FlagRPCLaddr)
	common.RPCCall(rpcLaddr, service.GetExecutedBlockByNumber, param,
		func(res interface{}) {
			obj := res.(service.JSONRPCResp)
			val, _ := util.Bytes2Struct(&protosledger.ExecutedBlock{}, obj.Payload)
			hashOpExecBlk(val.(*protosledger.ExecutedBlock), exclude)
			common.PrintJSON(common.FromPbExecutedBlock(val.(*protosledger.ExecutedBlock)))
		})
}

func isSynced(cmd *cobra.Command, args []string) {
	rpcLaddr, _ := cmd.Flags().GetString(common.FlagRPCLaddr)
	common.RPCCall(rpcLaddr, service.IsSynced, nil, func(res interface{}) {
		obj := res.(service.JSONRPCResp)
		val, _ := strconv.ParseBool(string(obj.Payload))
		common.PrintJSON(val)
	})
}

func hashOp(hash []byte) []byte {
	return hash
}

func hashOpTxRes(tx *protosledger.TransactionExecResult) {
	tx.Receipt.TxHash = hashOp(tx.Receipt.TxHash)
}

func hashOpHeader(header *protosledger.BlockHeader) {
	header.ParentHash = hashOp(header.ParentHash)
	header.StateHashOfCheckBlock = hashOp(header.StateHashOfCheckBlock)
	header.TxRootHash = hashOp(header.TxRootHash)
}

func hashOpBlkExecResult(exeResult *protosledger.BlockExecResult) {
	exeResult.StateHash = hashOp(exeResult.StateHash)
	txs := exeResult.TransactionExecResults
	for _, item := range txs {
		hashOpTxRes(item)
	}
}

func hashOpBlk(blk *protosledger.Block, exclude bool) {
	hashOpHeader(blk.Header)
	if exclude {
		blk.Transactions = nil
	}
}

func hashOpExecBlk(block *protosledger.ExecutedBlock, exclude bool) {
	hashOpBlk(block.Block, exclude)
	hashOpBlkExecResult(block.BlockExecResult)
}
